package com.gitee.taotaojs.mybootsuper.config.interceptor;

import com.gitee.taotaojs.exception.MyException;
import com.gitee.taotaojs.mybootsuper.bean.BasePageBean;
import com.gitee.taotaojs.mybootsuper.mapper.MySqlBuild;
import lombok.extern.log4j.Log4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.springframework.beans.factory.annotation.Value;

import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;

/**
 * @author TaoTaojs
 * @date 2020/8/21 11:16 AM
 * <p>Description</p>
 * <h2>功能描述</h2>
 * 分页拦截器
 * <h3>主要功能：</h3>
 * <ol>
 *     <li>直接将Mapper方法中传入BasePageBean对象，且此对象中的useFlag将直接执行分页方法</li>
 *     <li>在Mapper方法注入useFlag，即可自定义Mapper方法是否需要增加分页功能</li>
 * </ol>
 * <h3>关联类</h3>
 * <p>参考链接：</p>
 * <h2>更新日志</h2>
 * [author]        [time]                      [version]          [desc]
 * TaoTaojs        2020/8/21 11:16 AM             V1.0            基础方法完成
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
@Log4j
public class MyPageInterceptor implements Interceptor {

    /** 每页显示的条目数 */
    @Value("${default.pageSize:10}")
    private int defaultPageSize;
    /** 当前现实的页数 */
    @Value("${default.page:1}")
    private int defaultPage;

    /** 是否启用分页功能 */
    @Value("${default.useFlag:false}")
    private boolean defaultUseFlag;
    /** 检测当前页码的合法性（大于最大页码或小于最小页码都不合法）*/
    @Value("${default.checkFlag:false}")
    private boolean defaultCheckFlag;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取StatementHandler，默认是RoutingStatementHandler
        StatementHandler statementHandler = getActuralHandlerObject(invocation);
        //获取statementHandlmer包装类
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        //获取sql
        String sql = statementHandler.getBoundSql().getSql();

        // 检测未通过，不是select语句
        if (!checkIsSelectFalg(sql)) {
            return invocation.proceed();
        }

        //获取执行sql的相关信息对象
        BoundSql boundSql = statementHandler.getBoundSql();
        //获取入参的对象
        Object paramObject = boundSql.getParameterObject();

        //获取自定义sql语句构造对象，具体看getPageParam方法
        MySqlBuild<?> mySqlBuild = getPageParam(paramObject);
        //如果没有分页参数，就不分页
        if(mySqlBuild == null){
            return invocation.proceed();
        }

        //获取传参对象
        BasePageBean basePageBean = mySqlBuild.getBasePageBean();
        //如果没有分页参数，就不分页
        if(basePageBean == null || basePageBean.getUseFlag() == null){
            return invocation.proceed();
        }

        Integer pageNum = basePageBean.getPage() == null ? defaultPage : basePageBean.getPage();
        Integer pageSize = basePageBean.getPageSize() == null ? defaultPageSize : basePageBean.getPageSize();

        Boolean useFlag = basePageBean.getUseFlag() == null ? defaultUseFlag : basePageBean.getUseFlag();
        Boolean checkFlag = basePageBean.getCheckFlag() == null ? defaultCheckFlag : basePageBean.getCheckFlag();

        //获取mapper方法名称
//        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
//        String mapId = mappedStatement.getId();

        // 不使用分页功能
//        if (!useFlag || !mapId.matches(".+ByPage$")) {
        if (Boolean.FALSE.equals(useFlag)) {
            return invocation.proceed();
        }

        int totle = getTotle(invocation, metaStatementHandler, boundSql);

        // 将动态获取到的分页参数回填到pageParam中
        setTotltToParam(basePageBean, totle, pageSize);

        // 检查当前页码的有效性
        checkPage(checkFlag, pageNum, basePageBean.getTotlePage());

        // 修改sql
        return updateSql2Limit(invocation, metaStatementHandler, sql, pageNum, pageSize);
    }


    /**
     * 获取代理对象
     * @param o 对象
     * @return 动态代理对象
     */
    @Override
    public Object plugin(Object o) {
        //生成object对象的动态代理对象
        return Plugin.wrap(o, this);
    }

    /**
     * 在配置插件的时候配置默认参数
     * 直接使用@Value来设置默认值
     * @param properties spring读取的配置信息
     */
    @Override
    public void setProperties(Properties properties) {
        String strDefaultPage = properties.getProperty("default.page");
        String strDefaultPageSize = properties.getProperty("default.pageSize");
        String strDefaultUseFlag = properties.getProperty("default.useFlag");
        String strDefaultCheckFlag = properties.getProperty("default.checkFlag");
        if(strDefaultPage != null) {
            defaultPage = Integer.valueOf(strDefaultPage, 1);
        } else {
            defaultPage = 1;
        }
        if(strDefaultPageSize != null) {
            defaultPageSize = Integer.valueOf(strDefaultPageSize);
        } else {
            defaultPageSize = 10;
        }
        if(strDefaultUseFlag != null) {
            defaultUseFlag = Boolean.valueOf(strDefaultUseFlag);
        } else {
            defaultUseFlag = true;
        }
        if (strDefaultCheckFlag != null) {
            defaultCheckFlag = Boolean.valueOf(strDefaultCheckFlag);
        } else {
            defaultCheckFlag = true;
        }
    }

    /**
     * 从代理对象中分离出真实statementHandler对象,非代理对象
     * @param invocation a
     * @return 自定义数据包装类
     */
    private StatementHandler getActuralHandlerObject(Invocation invocation) {

        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        Object object = null;

        // 分离代理对象链，目标可能被多个拦截器拦截，分离出最原始的目标类
        while (metaStatementHandler.hasGetter("h")) {
            object = metaStatementHandler.getValue("h");
            metaStatementHandler = SystemMetaObject.forObject(object);
        }

        if (object == null) {
            return statementHandler;
        }

        return (StatementHandler) object;
    }

    /**
     * 判断是否是select语句，只有select语句，才会用到分页
     * @param sql sql
     * @return 是select则返回
     */
    private boolean checkIsSelectFalg(String sql) {
        String trimSql = sql.trim().toLowerCase();
        int index = trimSql.indexOf("select");
        return index == 0;
    }

    /**
     * 获取分页的参数
     * 参数可以通过map，@param注解进行参数传递。或者请求pojo继承自PageParam，将PageParam中的分页数据放进去
     * @param paramerObject 页面传参
     * @return 返回分页对象
     */
    private MySqlBuild<?> getPageParam(Object paramerObject) {

        if (paramerObject == null) {
            return null;
        }

        MySqlBuild<?> mySqlBuild = null;
        // 通过map和@param注解将PageParam参数传递进来，pojo继承自PageParam不推荐使用  这里从参数中提取出传递进来的pojo继承自PageParam

        // 首先处理传递进来的是map对象和通过注解方式传值的情况，从中提取出PageParam,循环获取map中的键值对，取出PageParam对象
        if (paramerObject instanceof Map) {
            Map<String, Object> params = (Map<String, Object>) paramerObject;
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                if (entry.getValue() instanceof MySqlBuild) {
                    return (MySqlBuild) entry.getValue();
                }
            }
        } else if (paramerObject instanceof MySqlBuild) {
            // 继承方式 pojo继承自PageParam 只取出我们希望得到的分页参数
            mySqlBuild = (MySqlBuild) paramerObject;

        }

        return mySqlBuild;
    }

    /**
     * 获取当前sql查询的记录总数
     * @param invocation 调用
     * @param metaStatementHandler 元对象处理
     * @param boundSql sql
     * @return 获取查询记录的总数
     */
    private int getTotle(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql) {

        // 获取mapper文件中当前查询语句的配置信息
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");

        // 获取所有配置Configuration
        org.apache.ibatis.session.Configuration configuration = mappedStatement.getConfiguration();

        // 获取当前查询语句的sql
        String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");

        // 将sql改写成统计记录数的sql语句,这里是mysql的改写语句,将第一次查询结果作为第二次查询的表
        String countSql = "select count(*) as totle from (" + sql + ") $_paging";

        // 获取connection连接对象，用于执行countsql语句
        Connection conn = (Connection) invocation.getArgs()[0];

        PreparedStatement ps = null;
        ResultSet rs = null;

        int totle = 0;

        try {
            // 预编译统计总记录数的sql
            ps = conn.prepareStatement(countSql);

            // 构建统计总记录数的BoundSql
            BoundSql countBoundSql = new BoundSql(configuration, countSql, boundSql.getParameterMappings(), boundSql.getParameterObject());

            // 构建ParameterHandler，用于设置统计sql的参数
            ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), countBoundSql);

            // 设置总数sql的参数
            parameterHandler.setParameters(ps);

            // 执行查询语句
            rs = ps.executeQuery();
            while (rs.next()) {
                // 与countSql中设置的别名对应
                totle = rs.getInt("totle");
            }
        } catch (SQLException e) {
            log.error(e);
        } finally {
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    log.error(e);
                }
            }
            if(rs != null){
                try {
                    rs.close();
                } catch (SQLException e){
                    log.error(e);
                }
            }
        }

        return totle;
    }

    /**
     * 设置条数参数到pageparam对象，设置分页之后的总页数
     * @param param 分页字段
     * @param totle 总记录数量
     * @param pageSize 每页数量
     */
    private void setTotltToParam(BasePageBean param, int totle, int pageSize) {
        param.setTotle(totle);
        param.setTotlePage(totle % pageSize == 0 ? totle / pageSize : (totle / pageSize) + 1);
    }


    /**
     * 验证当前页码的有效性
     * @param checkFlag 是否检测当前页码的合法性
     * @param pageNumber 当前页数
     * @param pageTotle 分页后的总页数
     * @throws Exception 验证失败抛出异常
     */
    private void checkPage(boolean checkFlag, Integer pageNumber, Integer pageTotle) {
        if (checkFlag && pageNumber > pageTotle) {
            throw new MyException("查询失败，查询页码" + pageNumber + "大于总页数" + pageTotle);
        }
    }

    /**
     * 修改原始sql语句为分页sql语句
     * @param invocation 拦截对象
     * @param metaStatementHandler 数据获取类
     * @param sql sql语句
     * @param page 当前页
     * @param pageSize 每页大小
     * @return 增加了分页语句的sql
     * @throws InvocationTargetException 异常
     * @throws IllegalAccessException 异常
     * @throws SQLException sql异常
     */
    private Object updateSql2Limit(Invocation invocation, MetaObject metaStatementHandler, String sql, int page, int pageSize) throws InvocationTargetException, IllegalAccessException, SQLException {

        // 构建新的分页sql语句
        String limitSql = "select * from (" + sql + ") $_paging_table limit ?,?";
        //后期优化速度
        //SELECT * FROM product a JOIN (select id from product limit 866613, 20) b ON a.ID = b.id
        //SELECT * FROM product WHERE ID > =(select id from product limit 866613, 1) limit 20

        // 修改当前要执行的sql语句
        metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);

        // 相当于调用prepare方法，预编译sql并且加入参数，但是少了分页的两个参数，它返回一个PreparedStatement对象
        PreparedStatement ps = (PreparedStatement) invocation.proceed();

        // 获取sql总的参数总数
        int count = ps.getParameterMetaData().getParameterCount();

        // 设置与分页相关的两个参数
        ps.setInt(count - 1, (page - 1) * pageSize);
        ps.setInt(count, pageSize);

        return ps;
    }


}
